{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c5f562db",
   "metadata": {},
   "source": [
    "# Rotational Trading\n",
    "\n",
    "Rotational trading involves purchasing the best-performing assets while selling underperforming ones. As you may have guessed, **PyBroker** is an excellent tool for backtesting such strategies. So, let's dive in and get started with testing our rotational trading strategy!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "e174e6d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pybroker as pyb\n",
    "from pybroker import ExecContext, Strategy, StrategyConfig, YFinance"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6475b3fc",
   "metadata": {},
   "source": [
    "Our strategy will involve ranking and buying stocks with the highest [price rate-of-change (ROC)](https://www.investopedia.com/terms/p/pricerateofchange.asp). To start, we'll define a 20-day ROC indicator using [TA-Lib](https://github.com/TA-Lib/ta-lib-python):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "20e6f772",
   "metadata": {},
   "outputs": [],
   "source": [
    "import talib as ta\n",
    "\n",
    "roc_20 = pyb.indicator(\n",
    "    'roc_20', lambda data: ta.ROC(data.adj_close, timeperiod=20))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce92ab84",
   "metadata": {},
   "source": [
    "Next, let's define the rules of our strategy:\n",
    "\n",
    "- Buy the two stocks with the highest 20-day ROC.\n",
    "- Allocate 50% of our capital to each stock.\n",
    "- If either of the stocks is no longer ranked among the top five 20-day ROCs, then we will liquidate that stock.\n",
    "- Trade these rules daily.\n",
    "\n",
    "Let’s set up our config and some parameters for the above rules:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "0ee3887a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "config = StrategyConfig(max_long_positions=2)\n",
    "pyb.param('target_size', 1 / config.max_long_positions)\n",
    "pyb.param('rank_threshold', 5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7350345c",
   "metadata": {},
   "source": [
    "To proceed with our strategy, we will implement a ``rank`` function that ranks each stock by their 20-day ROC in descending order, from highest to lowest."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "dbc19aa4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def rank(ctxs: dict[str, ExecContext]):\n",
    "    scores = {\n",
    "        symbol: ctx.indicator('roc_20')[-1]\n",
    "        for symbol, ctx in ctxs.items()\n",
    "    }\n",
    "    sorted_scores = sorted(\n",
    "        scores.items(), \n",
    "        key=lambda score: score[1],\n",
    "        reverse=True\n",
    "    )\n",
    "    threshold = pyb.param('rank_threshold')\n",
    "    top_scores = sorted_scores[:threshold]\n",
    "    top_symbols = [score[0] for score in top_scores]\n",
    "    pyb.param('top_symbols', top_symbols)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3634399",
   "metadata": {},
   "source": [
    "The ``top_symbols`` global parameter contains the symbols of the stocks with the top five highest 20-day ROCs.\n",
    "\n",
    "Now that we have a method for ranking stocks by their ROC, we can proceed with implementing a ``rotate`` function to manage the rotational trading."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "e26bbffb",
   "metadata": {},
   "outputs": [],
   "source": [
    "def rotate(ctx: ExecContext):\n",
    "    if ctx.long_pos():\n",
    "        if ctx.symbol not in pyb.param('top_symbols'):\n",
    "            ctx.sell_all_shares()\n",
    "    else:\n",
    "        target_size = pyb.param('target_size')\n",
    "        ctx.buy_shares = ctx.calc_target_shares(target_size)\n",
    "        ctx.score = ctx.indicator('roc_20')[-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e2da6b56",
   "metadata": {},
   "source": [
    "We liquidate the currently held stock if it is no longer ranked among the top five 20-day ROCs. Otherwise, we rank all stocks by their 20-day ROC and buy up to the top two ranked. For more information on ranking when placing buy orders, see the [Ranking and Position Sizing notebook](https://www.pybroker.com/en/latest/notebooks/4.%20Ranking%20and%20Position%20Sizing.html).\n",
    "\n",
    "We will use the [set_before_exec](https://www.pybroker.com/en/latest/reference/pybroker.strategy.html#pybroker.strategy.Strategy.set_before_exec) method to execute our ranking with ``rank`` before running the ``rotate`` function. For this backtest, we will use a universe of 10 stocks:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "33f90b28",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00\n",
      "\n",
      "Loading bar data...\n",
      "[*********************100%***********************]  10 of 10 completed\n",
      "Loaded bar data: 0:00:03 \n",
      "\n",
      "Computing indicators...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100% (10 of 10) |########################| Elapsed Time: 0:00:00 Time:  0:00:00\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time:  0:00:00\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Finished backtest: 0:00:06\n"
     ]
    }
   ],
   "source": [
    "strategy = Strategy(\n",
    "    YFinance(), \n",
    "    start_date='1/1/2018', \n",
    "    end_date='1/1/2023', \n",
    "    config=config\n",
    ")\n",
    "strategy.set_before_exec(rank)\n",
    "strategy.add_execution(rotate, [\n",
    "    'TSLA', \n",
    "    'NFLX', \n",
    "    'AAPL', \n",
    "    'NVDA', \n",
    "    'AMZN',\n",
    "    'MSFT',\n",
    "    'GOOG',\n",
    "    'AMD',\n",
    "    'INTC',\n",
    "    'META'\n",
    "], indicators=roc_20)\n",
    "result = strategy.backtest(warmup=20)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "da570475",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>type</th>\n",
       "      <th>symbol</th>\n",
       "      <th>date</th>\n",
       "      <th>shares</th>\n",
       "      <th>limit_price</th>\n",
       "      <th>fill_price</th>\n",
       "      <th>fees</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>id</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>buy</td>\n",
       "      <td>NFLX</td>\n",
       "      <td>2018-02-01</td>\n",
       "      <td>184</td>\n",
       "      <td>NaN</td>\n",
       "      <td>267.67</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>buy</td>\n",
       "      <td>AMD</td>\n",
       "      <td>2018-02-01</td>\n",
       "      <td>3639</td>\n",
       "      <td>NaN</td>\n",
       "      <td>13.53</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>sell</td>\n",
       "      <td>AMD</td>\n",
       "      <td>2018-02-05</td>\n",
       "      <td>3639</td>\n",
       "      <td>NaN</td>\n",
       "      <td>11.56</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>buy</td>\n",
       "      <td>AMZN</td>\n",
       "      <td>2018-02-05</td>\n",
       "      <td>627</td>\n",
       "      <td>NaN</td>\n",
       "      <td>69.49</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>sell</td>\n",
       "      <td>AMZN</td>\n",
       "      <td>2018-04-03</td>\n",
       "      <td>627</td>\n",
       "      <td>NaN</td>\n",
       "      <td>69.23</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>256</th>\n",
       "      <td>buy</td>\n",
       "      <td>AMD</td>\n",
       "      <td>2022-11-21</td>\n",
       "      <td>3589</td>\n",
       "      <td>NaN</td>\n",
       "      <td>72.28</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>257</th>\n",
       "      <td>sell</td>\n",
       "      <td>AMD</td>\n",
       "      <td>2022-12-14</td>\n",
       "      <td>3589</td>\n",
       "      <td>NaN</td>\n",
       "      <td>70.16</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>258</th>\n",
       "      <td>buy</td>\n",
       "      <td>NFLX</td>\n",
       "      <td>2022-12-14</td>\n",
       "      <td>881</td>\n",
       "      <td>NaN</td>\n",
       "      <td>319.57</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>259</th>\n",
       "      <td>sell</td>\n",
       "      <td>NVDA</td>\n",
       "      <td>2022-12-28</td>\n",
       "      <td>1491</td>\n",
       "      <td>NaN</td>\n",
       "      <td>140.73</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>260</th>\n",
       "      <td>buy</td>\n",
       "      <td>META</td>\n",
       "      <td>2022-12-28</td>\n",
       "      <td>1869</td>\n",
       "      <td>NaN</td>\n",
       "      <td>116.83</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>260 rows × 7 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "     type symbol       date  shares  limit_price  fill_price  fees\n",
       "id                                                                \n",
       "1     buy   NFLX 2018-02-01     184          NaN      267.67   0.0\n",
       "2     buy    AMD 2018-02-01    3639          NaN       13.53   0.0\n",
       "3    sell    AMD 2018-02-05    3639          NaN       11.56   0.0\n",
       "4     buy   AMZN 2018-02-05     627          NaN       69.49   0.0\n",
       "5    sell   AMZN 2018-04-03     627          NaN       69.23   0.0\n",
       "..    ...    ...        ...     ...          ...         ...   ...\n",
       "256   buy    AMD 2022-11-21    3589          NaN       72.28   0.0\n",
       "257  sell    AMD 2022-12-14    3589          NaN       70.16   0.0\n",
       "258   buy   NFLX 2022-12-14     881          NaN      319.57   0.0\n",
       "259  sell   NVDA 2022-12-28    1491          NaN      140.73   0.0\n",
       "260   buy   META 2022-12-28    1869          NaN      116.83   0.0\n",
       "\n",
       "[260 rows x 7 columns]"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "result.orders"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
